<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="root">
        <div>
            <div>{{name}}</div>
            <div>{{age}}</div>
            <div>hello3</div>
            <ul>
                <li>li1</li>
                <li>li2</li>
                <li>li3</li>
            </ul>
        </div>
    </div>
    <script src="./watcher.js"></script>
    <script src="./Dep.js"></script>
    
    <script>
        function JGVue(options){
            this._data = options.data
            let elm = document.querySelector(options.el)
            this._template = elm
            this._parent = elm.parentNode

            reacity(this._data,this)  //第二个参数是在将JGVue的实例传入

            this.initData()//将响应式的数据进行代理，将数据代理到JGVue的实例上

            //挂载函数
            this.mount()
        }

        JGVue.prototype.mount = function(){
            //提供一个render函数，来生成虚拟dom
            this.render = this.createRenderFn()

            this.mountComponent()
        }
        JGVue.prototype.mountComponent = function(){
            let mount = () => {
                this.update(this.render())
            }

            //mount.call(this)

            // 这个 Watcher 就是全局的 Watcher, 在任何一个位置都可以访问他了 ( 简化的写法 )
            new Watcher( this, mount ); // 相当于这里调用了 mount
        }
        JGVue.prototype.createRenderFn = function(){
            let ast = getVNode(this._template)
            return function render(){
                let _tpm = compiler(ast,this._data)
                console.log(_tpm)
                return _tpm
            }
        }
        JGVue.prototype.update = function(vnode){
            let realDOM = parseNode(vnode)
            console.log(realDOM)
            this._parent.replaceChild(realDOM,document.querySelector('#root'))
        }

        //虚拟dom构造函数
        class VNode{
            constructor(tag,data,value,type){
                this.tag = tag && tag.toLowerCase()
                this.data = data
                this.value = value
                this.type = type
                this.children = []
            }
            appendChild(vnode){
                this.children.push(vnode)
            }
        }

        //根据真实dom结构生成虚拟dom
        function getVNode(node){
            let nodeType = node.nodeType
            let _vnode = null
            if(nodeType === 1){
                let nodeName = node.nodeName
                let attrs = node.attributes
                let _attrsObj = {}
                for(let i = 0; i < attrs.length; i++){
                    _attrsObj[attrs[i].nodeName] = attrs[i].nodeValue
                }
                _vnode = new VNode(nodeName,_attrsObj,undefined,nodeType)
                let childNodes = node.childNodes
                for(let j = 0; j < childNodes.length; j++){
                    _vnode.appendChild(getVNode(childNodes[j]))
                }
            }else if(nodeType === 3){
                _vnode = new VNode(undefined,undefined,node.nodeValue,nodeType)
            }
            return _vnode
        }
        
        //将数据填充到虚拟dom中
        function compiler(vnode,data){
            let _tag = vnode.tag
            let _data = vnode.data
            let _value = vnode.value
            let _type = vnode.type
            let _children = vnode.children

            let _vnode = null

            if(_type === 1){ //元素节点
                _vnode = new VNode(_tag,_data,_value,_type)
                _children.forEach(_subchild => {
                    _vnode.appendChild(compiler(_subchild,data))
                });
            }else if(_type === 3){  //文本节点
                _value = _value.replace(r,function(a,b){
                    return getValueByPath(data,b.trim())
                })

                _vnode = new VNode(_tag,_data,_value,_type)
            }
            return _vnode
        }

        //根据路径获取对象任意层级的属性
        function getValueByPath(obj,path){
            let paths = path.split(',')
            let res = obj
            let prop
            while(prop = paths.shift()){
                res = res[prop]
            }
            return res
        }

        //正则
        let r = /\{\{(.+?)\}\}/g

        //将虚拟dom转换成真实dom
        function parseNode(vnode){
            let type = vnode.type
            let _node = null
            if(type === 3){
                return document.createTextNode(vnode.value)
            }else if(type === 1){
                _node = document.createElement(vnode.tag)                            
                let data = vnode.data
                Object.keys(data).forEach(key => {
                    let attrsName = key
                    let attrsValue = data[key]
                    _node.setAttribute(attrsName,attrsValue)
                })
                let children = vnode.children
                children.forEach(vChild => {
                    _node.appendChild(parseNode(vChild))
                })
                return _node
            }
            
        }

        
        function defineReactive(target,key,value,enumerable){
            let that = this
            //判断value值类型，入时非数组的引用类型，就要进行递归
            if(typeof value === 'object' && value != null){
                reacity(value)
            }
            let dep = new Dep()
            Object.defineProperty(target,key,{
                configurable:true,
                enumerable: !!enumerable,
                get(){  //当访问目标对象的属性时就会触发
                    dep.depend()
                    console.log(`读取了${target}的属性${key}`)
                    return value
                },
                set(newvalue){ // 当修改目标对象的某个属性时就会触发
                    console.log(`设置了${target}的${key}属性值为${newvalue}`)
                    if(typeof newvalue === 'object' && newvalue != null){
                        reacity(newvalue)
                    }
                    value = newvalue
                    
                    /* //需要在这里刷新页面将重新赋值后的数据变为响应式
                    that.mountComponent() */

                    // 派发更新, 找到全局的 watcher, 调用 update
                    dep.notify();
                }
            })
        }

        //将o响应式化
        /* function reacity(o, vm){
            let keys = Object.keys(o)
            for(let i = 0; i < keys.length; i++){
                let key = keys[i]
                let value = o[key]
                //判断传入的属性值是否为引用类型
                if(Array.isArray(value)){

                    //将数组的元素变成响应式的
                    value.__proto__ = array_methods
                    //对数组类型的数据进行处理
                    for(let j = 0; j < value.length; j++){
                        reacity(value[j],vm)
                    }
                }else{
                    defineReactive.call(vm,o,key,value,true) //call
                }
            }
            
        } */
        function reacity(o, vm){
            if(Array.isArray(o)){
                o.__proto__ = array_methods
                for(let i = 0;i < o.length; i++){
                    reacity(o[i],vm)
                }
            }else{
                let keys = Object.keys(o)
                for(let j = 0 ; j < keys.length; j++){
                    let prop = keys[j]
                    defineReactive.call(vm,o,prop,o[prop],true)
                }
            }
            
        }

        let ARRAT_METHODS = [
            'push',
            'pop',
            'reverse',
            'shift',
            'unshift',
            'splice',
        ]

        //创建了一个array_methods对象，让其原型继承于Array_prototype
        let array_methods = Object.create(Array.prototype)
        //重写数组
        ARRAT_METHODS.forEach(method => {
            //调用原数组原型上处理数组的方法
            array_methods[method] = function(){
                console.log('调用的是拦截的' + method + '方法')
                //在这里要再次调用reacity来将调用数组方法传入的参数响应式化
                for(let i = 0; i < arguments.length; i++){
                    reacity(arguments[i])
                }
                let  res = Array.prototype[method].apply(this,arguments)
                return res
            }
        })

        //让数据变成响应式的
        JGVue.prototype.initData = function(){
            let keys = Object.keys(this._data) 
            for(let i = 0; i < keys.length; i++){
                reacity(this._data,this)
            }

            //实现代理，就是将this._data[keys[i]]这个属性映射到this[keys[i]]上
            //相当于在访问this.[keys[i]]时，其实访问的是this._data[keys[i]]
            //让this（JGVue实例）提供keys[i]属性
            /* for(let i = 0; i < keys.length; i++){
                Object.defineProperty(this,keys[i],{
                    enumerable:true,
                    configurable:true,
                    get(){
                        return this._data[keys[i]]
                    },
                    set(newvalue){
                        this._data[keys[i]] = newvalue
                    }
                })
            } */
            for(let i = 0; i < keys.length; i++){
                proxy(this,'_data',keys[i])
            }
        }

        //这个代理的具体思想就是：
        //将访问target对象身上的key属性时映射到访问target的prop属性的key身上
        //例如调用该函数时：proxy（this，‘_data’,key）就是将访问this（JGVue实例）身上的key属性映射到this对象的_data属性的key身上

        function proxy(target,prop,key){
            Object.defineProperty(target,key,{
                enumerable:true,
                configurable:true,
                get(){
                    return target[prop][key]
                },
                set(newvalue){
                    target[prop][key] = newvalue
                }
            })
        }

        let app = new JGVue({
            el:'#root',
            data:{
                name:'张三',
                age:'18',
                datas:[
                    {name:'语文'},
                    {name:'英语'}
                ]
            }
        })

        /* 
        上次代码要改进的一个地方是：当给数据重新赋值时，要将这个重新赋过来的值也变成响应式的，
        主要思路是：重新赋值时，让页面刷新，在页面刷新的过程中就会触发defineReactive函数以及
        Object.defineProperty()来读取页面数据，就会触发get函数，将数据变成响应式。
        要想使页面刷新，就要调用mount函数去执行mountcomponent函数进而调用update函数更新页面。
        而这些函数是在JGVue构造函数原型上的，所以必须要在处理数据响应式的函数defineReactive函数
        中得到JGVue构造函数的实例，即把JGVue实例对象作为参数传入。
        */

        
    </script>
</body>
</html>